-module(types).
-compile([export_all]).
-import(util, [pow2/1, pr/1, pr/2]).

%% We want to test that values from valid range are accepted and
%% ones out of the range are rejected with badarg
%% XXX describe with more detail what this test does


expected() ->
    test_output() ++ test_output().

%%% TEST big unsigned result values (int_max < x <uint_max)

main() ->
    %% LINUX {ok, Lib} = ffi:load_library("build/types.so"),
    {ok, Lib} = ffi:load_library("build/types"),
    regr(Lib),
    test(fun(Fun, Args, Params) -> ffi:call_block(Lib, Fun, Args, Params) end),
    test(fun(Fun, Args, Params) ->       ffi:call(Lib, Fun, Args, Params) end),
    util:die().

regr(Lib) ->
    %% forgot to read NIL from end of the list
    3000 = ffi:call_block(Lib,
                          "add_int",
                          [{int, 1000}, {int, 2000}],
                          [{res_type, int}]),
    %% don't remember what this was about
    Int64_Min = -pow2(63),
    Int64_Min = ffi:call_block(Lib, "add_int64",
                               [{int64, 1}, {int64, pow2(63)-1}],
                               {res_type, int64}),
    %% fix_args for supporting integer values of type double
    5.0 = ffi:call_block(Lib,
                         "add_double",
                         [{double, 2}, {double, 3}],
                         {res_type, double}),
    %% fix_async_res for overcoming ERL_DRV_UINT absence
    Uint32_Big = pow2(32)-11,
    Uint32_Big = ffi:call(Lib,
                   "add_uint32",
                   [{uint32, Uint32_Big-5}, {uint32, 5}],
                   {res_type, uint32}),
    %%sending int64 as tuple of 2 ERL_DRV_INTs
    P34 = pow2(34),
    P34 = ffi:call(Lib,
                   "add_int64",
                   [{int64, pow2(33)}, {int64, pow2(33)}],
                   {res_type, int64}),
    N34 = -P34,
    N34 = ffi:call(Lib,
                   "add_int64",
                   [{int64, -pow2(33)}, {int64, -pow2(33)}],
                   {res_type, int64}),
    Uint32_Big = ffi:call(Lib,
                          "add_uint64",
                          [{uint64, Uint32_Big-5}, {uint64, 5}],
                          {res_type, uint64}),
    Int64_Max = pow2(63) - 1,
    Int64_Max = ffi:call(Lib, "add_int64",
                         [{int64, (Int64_Max div 2) + 1}, {int64, Int64_Max div 2}],
                         {res_type, int64}).

all_ok([]) -> ok;
all_ok([ok|Rest]) -> all_ok(Rest).

func_type_list([]) -> [];
func_type_list([Type|Rest]) ->
    [{"add_" ++ atom_to_list(Type), Type}|func_type_list(Rest)].

test(Call3) ->
    25 = Call3("return25", [], []),
    25 = Call3("return25", [], {res_type, int}),
    25 = Call3("return25", [], [{res_type, int}]),
    all_ok([test_int(Call3, Func, Type) ||
               {Func, Type} <- func_type_list(all_ints())]),
    all_ok([test_double(Call3, Func, Type) ||
               {Func, Type} <- func_type_list(floats())]).

test_double(Call3, Func, Type) ->
    pr(Type),
    3.0 = Call3(Func, [{Type, 1.0}, {Type, 2.0}], {res_type, Type}),
    -3.0 = Call3(Func, [{Type, -1.0}, {Type, -2.0}], {res_type, Type}),
    test_invalid_type(Call3, Func, Type),
    test_defdouble(Call3, Func, Type).                 

test_defdouble(Call3, Func, double) ->
    3.0 = Call3(Func, [1.0, 2.0], {res_type, double}),
    -3.0 = Call3(Func, [-1.0, -2.0], {res_type, double}),
    ok;

test_defdouble(_Call3, _Func, _Type) -> ok.

test_int(Call3, Func, Type) ->
    pr(Type),
    test_int_minmax(fun (A, B) -> Call3(Func,
                                    [{Type, A}, {Type, B}],
                                    {res_type, Type}) end,
                       range(Type)),
    {badarg, 2} = (catch {ok, Call3(Func, [{Type, 5}, {Type, 3.141}], [])}),
    test_invalid_type(Call3, Func, Type),
    test_defint(Call3, Func, Type).

test_defint(Call3, Func, int) ->
    test_int_minmax(fun (A, B) -> Call3(Func,
                                            [A, B],
                                            []) end,
                        range(int));

test_defint(_Call3, _Func, _Type) -> ok.

%%% test_add_int_minmax/2 will test out-of-range integer values,
%%% test_invalid_type will test ones of wrong type
%%% Note: Substituting one valid type with another valid type (say, int8
%%% with int16) won't be detected and will crash the Erlang process (i.e. node)
    
test_invalid_type(Call3, Func, Type) ->
    {badarg, 1} = (catch {ok, Call3(Func, [shit, {Type, 5}], [])}),
    {badarg, 1} = (catch {ok, Call3(Func, [{Type, "hello"}, {Type, 5}], [])}),
    {badarg, 1} = (catch {ok, Call3(Func, [{Type, hello}, {Type, 5}], [])}),
    {badarg, 1} = (catch {ok, Call3(Func, [{Type, {}}, {Type, 5}], [])}),
    {badarg, 2} = (catch {ok, Call3(Func, [{Type, 5}, {Type, []}], [])}),
    {badarg, 2} = (catch {ok, Call3(Func, [{Type, 5}, {Type, <<"hello">>}], [])}),
    {badarg, 2} = (catch {ok, Call3(Func, [{Type, 5}, {void, 2}], [])}),
    {badarg, 2} = (catch {ok, Call3(Func, [{Type, 5}, void], [])}),
    {badarg, 1} = (catch {ok, Call3(Func, [{atom_to_list(Type), 5}, void], [])}),
    {badarg, 1} = (catch {ok,
                          Call3(Func,
                                [{list_to_binary(atom_to_list(Type)), 5}, void],
                                [])
                         }).

test_int_minmax(Call2, {Min, Max}) ->
    %pr("test_int_minmax Min=~w Max=~w", [Min, Max]),
    3 = Call2(1, 2),
    Min = Call2(Max, 1),
    Min = Call2(1, Max),
    Min = Call2(Min div 2, Min div 2),    
    Max = Call2(1 + (Max div 2), Max div 2),
    {badarg, 1} = (catch {ok, Call2(Max+1, 1)}),
    {badarg, 1} = (catch {ok, Call2(pow2(65), 1)}),
    {badarg, 1} = (catch {ok, Call2(Min-1, 1)}),
    {badarg, 1} = (catch {ok, Call2(-pow2(65), 1)}),
    {badarg, 2} = (catch {ok, Call2(1, Max+1)}),
    {badarg, 2} = (catch {ok, Call2(1, pow2(65))}),
    {badarg, 2} = (catch {ok, Call2(1, Min-1)}),
    {badarg, 2} = (catch {ok, Call2(1, -pow2(65))}),
    {badarg, 1} = (catch {ok, Call2(Max+1, Min-1)}),
    case Min of
        0 -> ok;
        _ -> test_int_minmax_signed(Call2, {Min, Max})
    end.

test_int_minmax_signed(Call2, {Min, Max}) ->
    -3 = Call2(-1, -2),
    Max = Call2(Min, -1),
    Max = Call2(-1, Min),
    Min = Call2(1, Max),
    ok.

test_output() ->
    lists:map(fun atom_to_list/1, all_types()).

signed_range(Size) when is_integer(Size) ->
    {-pow2(Size-1), pow2(Size-1)-1}.

unsigned_range(Size) when is_integer(Size) ->
    {0, pow2(Size)-1}.

rangeb(int8) -> signed_range(8);
rangeb(int16) -> signed_range(16);
rangeb(int32) -> signed_range(32);
rangeb(int64) -> signed_range(64);
rangeb(uint8) -> unsigned_range(8);
rangeb(uint16) -> unsigned_range(16);
rangeb(uint32) -> unsigned_range(32);
rangeb(uint64) -> unsigned_range(64).

range(Type) -> rangeb(ffi:typeof(Type)).

signed_ints() -> [int8, int16, int32, int64].
unsigned_ints() -> [uint8, uint16, uint32, uint64].
base_ints() -> signed_ints() ++ unsigned_ints().
floats() -> [float, double, longdouble].
all_base_types() -> base_ints() ++ floats() ++ [ptr, void].

platform_ints() -> [byte, ubyte, short, ushort, int, uint,
                    long, ulong, longlong, ulonglong].

all_ints() -> base_ints() ++ platform_ints().
all_types() -> all_ints() ++ floats().

%% move stuff about types to ffi or to util (see also sizeof.erl)
